package it.uniroma2.exp;

import it.uniroma2.dtk.op.convolution.ShuffledCircularConvolution;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * @author Lorenzo Dell'Arciprete
 * 
 * Abstract class providing a basic parameter management for experiments
 */
public abstract class AbstractExperiment {
	
	public enum Parameters {VECTOR_SIZE, USE_POS, LEXICALIZED, COMPOSITION_TYPE, 
		RANDOM_OFFSET, LAMBDA, RANDOM_LABELS, RANDOM_DEGREE, RANDOM_NODES, CUSTOM};
	private ArrayList<Parameters> parameters = new ArrayList<Parameters>();
	private int[] customParameterMaxValues = new int[0];
	private boolean verbose = false;
	private boolean silent = false;
	
	protected PrintStream out = System.out;

	//Experiment parameters
	private int[] vectorSizeArray = new int[] {8192};
	private boolean[] usePosArray = new boolean[] {true};
	private boolean[] lexicalizedArray = new boolean[] {true};
	private Class<?>[] compositionTypeArray = new Class<?>[] {ShuffledCircularConvolution.class};
	private int[] randomOffsetArray = new int[] {0};
	private double[] lambdaArray = new double[] {1};
	private int[] randomTreeLabelsArray = new int[] {6};
	private int[] randomTreeMaxDegreeArray = new int[] {3};
	private int[] randomTreeNodesArray = new int[] {15};
	
	//Current experiment parameter
	protected int vectorSize;
	protected boolean usePos;
	protected boolean lexicalized;
	protected Class<?> compositionType;
	protected int randomOffset;
	protected double lambda;
	protected int randomTreeLabels;
	protected int randomTreeMaxDegree;
	protected int randomTreeNodes;
	protected int[] currentCustomParameters = null;
	
	public void runAll() {
		for (int a : vectorSizeArray) {
			vectorSize = a;
			for (boolean b : usePosArray) {
				usePos = b;
				for (boolean c : lexicalizedArray) {
					lexicalized = c;
					for (Class<?> d : compositionTypeArray) {
						compositionType = d;
						for (int e : randomOffsetArray) {
							randomOffset = e;
							for (double f : lambdaArray) {
								lambda = f;
								for (int g : randomTreeLabelsArray) {
									randomTreeLabels = g;
									for (int h : randomTreeMaxDegreeArray) {
										randomTreeMaxDegree = h;
										for (int i : randomTreeNodesArray) {
											randomTreeNodes = i;
											while (generateNextCustomParameterCombination())
												runPresentCombination();
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
	
	private void runPresentCombination() {
		if (verbose) {
			out.println();
			out.println("\t***** Executing experiment with following parameters *****");
			for (Parameters par : parameters) {
				if (par.equals(Parameters.VECTOR_SIZE))
					out.println("\t*Vector size = "+vectorSize);
				else if (par.equals(Parameters.USE_POS))
					out.println("\t*Use pos = "+usePos);
				else if (par.equals(Parameters.LEXICALIZED))
					out.println("\t*Lexicalized = "+lexicalized);
				else if (par.equals(Parameters.COMPOSITION_TYPE))
					out.println("\t*Composition type = "+compositionType);
				else if (par.equals(Parameters.RANDOM_OFFSET))
					out.println("\t*Random offset = "+randomOffset);
				else if (par.equals(Parameters.LAMBDA))
					out.println("\t*Lambda = "+lambda);
				else if (par.equals(Parameters.RANDOM_LABELS))
					out.println("\t*Random tree labels = "+randomTreeLabels);
				else if (par.equals(Parameters.RANDOM_DEGREE))
					out.println("\t*Random tree max degree = "+randomTreeMaxDegree);
				else if (par.equals(Parameters.RANDOM_NODES))
					out.println("\t*Random tree nodes = "+randomTreeNodes);
				else if (par.equals(Parameters.CUSTOM))
					out.println("\t*Custom parameters = "+Arrays.toString(currentCustomParameters));
			}
			out.println("\t**********************************************************");
			out.println();
		}
		else if (!silent) {
			out.print("\t*****");
			for (Parameters par : parameters) {
				if (par.equals(Parameters.VECTOR_SIZE))
					out.print("\t"+vectorSize);
				else if (par.equals(Parameters.USE_POS))
					out.print("\t"+usePos);
				else if (par.equals(Parameters.LEXICALIZED))
					out.print("\t"+lexicalized);
				else if (par.equals(Parameters.COMPOSITION_TYPE))
					out.print("\t"+compositionType);
				else if (par.equals(Parameters.RANDOM_OFFSET))
					out.print("\t"+randomOffset);
				else if (par.equals(Parameters.LAMBDA))
					out.print("\t"+lambda);
				else if (par.equals(Parameters.RANDOM_LABELS))
					out.print("\t"+randomTreeLabels);
				else if (par.equals(Parameters.RANDOM_DEGREE))
					out.print("\t"+randomTreeMaxDegree);
				else if (par.equals(Parameters.RANDOM_NODES))
					out.print("\t"+randomTreeNodes);
				else if (par.equals(Parameters.CUSTOM))
					out.print("\t"+Arrays.toString(currentCustomParameters));
			}
			out.println("\t*****");
		}
		try {
			runExperiment();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	abstract protected void runExperiment() throws Exception;
	
	public void setVerbose() {
		verbose = true;
		silent = false;
	}
	
	public void setSilent() {
		verbose = false;
		silent = true;
	}

	public void setVectorSizeArray(int[] vectorSizeArray) {
		parameters.add(Parameters.VECTOR_SIZE);
		this.vectorSizeArray = vectorSizeArray;
	}
	
	public void setVectorSize(int value) {
		setVectorSizeArray(new int[] {value});
	}

	public void setUsePosArray(boolean[] usePosArray) {
		parameters.add(Parameters.USE_POS);
		this.usePosArray = usePosArray;
	}
	
	public void setUsePos(boolean value) {
		setUsePosArray(new boolean[] {value});
	}

	public void setLexicalizedArray(boolean[] lexicalizedArray) {
		parameters.add(Parameters.LEXICALIZED);
		this.lexicalizedArray = lexicalizedArray;
	}
	
	public void setLexicalized(boolean value) {
		setLexicalizedArray(new boolean[] {value});
	}

	public void setCompositionTypeArray(Class<?>[] compositionTypeArray) {
		parameters.add(Parameters.COMPOSITION_TYPE);
		this.compositionTypeArray = compositionTypeArray;
	}
	
	public void setCompositionType(Class<?> value) {
		setCompositionTypeArray(new Class<?>[] {value});
	}

	public void setRandomOffsetArray(int[] randomOffsetArray) {
		parameters.add(Parameters.RANDOM_OFFSET);
		this.randomOffsetArray = randomOffsetArray;
	}
	
	public void setRandomOffset(int value) {
		setRandomOffsetArray(new int[] {value});
	}
	
	public void setRandomOffsetRange(int minValue, int numValues, int step) {
		if (numValues < 1)
			numValues = 1;
		int[] values = new int[numValues];
		for (int i=0; i<numValues; i++)
			values[i] = minValue + step*i;
		setRandomOffsetArray(values);
	}

	public void setLambdaArray(double[] lambdaArray) {
		parameters.add(Parameters.LAMBDA);
		this.lambdaArray = lambdaArray;
	}
	
	public void setLambda(double value) {
		setLambdaArray(new double[] {value});
	}
	
	public void setLambdaRange(double minValue, int numValues, double step) {
		if (numValues < 1)
			numValues = 1;
		double[] values = new double[numValues];
		for (int i=0; i<numValues; i++)
			values[i] = minValue + step*i;
		setLambdaArray(values);
	}

	public void setRandomTreeLabelsArray(int[] randomTreeLabelsArray) {
		parameters.add(Parameters.RANDOM_LABELS);
		this.randomTreeLabelsArray = randomTreeLabelsArray;
	}
	
	public void setRandomTreeLabels(int value) {
		setRandomTreeLabelsArray(new int[] {value});
	}
	
	public void setRandomTreeLabelsRange(int minValue, int numValues, int step) {
		if (numValues < 1)
			numValues = 1;
		int[] values = new int[numValues];
		for (int i=0; i<numValues; i++)
			values[i] = minValue + step*i;
		setRandomTreeLabelsArray(values);
	}

	public void setRandomTreeMaxDegreeArray(int[] randomTreeMaxDegreeArray) {
		parameters.add(Parameters.RANDOM_DEGREE);
		this.randomTreeMaxDegreeArray = randomTreeMaxDegreeArray;
	}
	
	public void setRandomTreeMaxDegree(int value) {
		setRandomTreeMaxDegreeArray(new int[] {value});
	}
	
	public void setRandomTreeMaxDegreeRange(int minValue, int numValues, int step) {
		if (numValues < 1)
			numValues = 1;
		int[] values = new int[numValues];
		for (int i=0; i<numValues; i++)
			values[i] = minValue + step*i;
		setRandomTreeMaxDegreeArray(values);
	}

	public void setRandomTreeNodesArray(int[] randomTreeNodesArray) {
		parameters.add(Parameters.RANDOM_NODES);
		this.randomTreeNodesArray = randomTreeNodesArray;
	}
	
	public void setRandomTreeNodes(int value) {
		setRandomTreeNodesArray(new int[] {value});
	}
	
	public void setRandomTreeNodesRange(int minValue, int numValues, int step) {
		if (numValues < 1)
			numValues = 1;
		int[] values = new int[numValues];
		for (int i=0; i<numValues; i++)
			values[i] = minValue + step*i;
		setRandomTreeNodesArray(values);
	}
	
	/**
	 * Through this method you can set a certain number of custom parameters the experiment should explore
	 * Example: if you have 3 custom parameters, which can take on 2, 4 and 3 values respectively, 
	 * you should invoke setCustomParameters(new int[] {2,4,3}).
	 * At each run, you can then retrieve the current parameter value index in currentCustomParameters[];
	 * it is responsibility of the implementing class to use it to access the actual value (e.g. from a list or an array)
	 * @param maxValues the array whose length indicates the number of custom parameters and whose i-th element indicates the possible values for i-th parameter 
	 */
	public void setCustomParameters(int[] maxValues) {
		parameters.add(Parameters.CUSTOM);
		this.customParameterMaxValues = maxValues;
	}
	
	/**
	 * Initializes currentCustomParameters[] indices to all 0, updates it to the next valid combination, 
	 * or finalizes the generation by setting it to null  
	 * @return true if a new valid combination has been produced, false if no more combinations are available
	 */
	private boolean generateNextCustomParameterCombination() {
		if (currentCustomParameters == null) {
			currentCustomParameters = new int[customParameterMaxValues.length];
			Arrays.fill(currentCustomParameters, 0);
			return true;
		}
		else {
			for (int i=0; i<currentCustomParameters.length; i++) {
				if (currentCustomParameters[i] < customParameterMaxValues[i]-1) {
					currentCustomParameters[i]++;
					return true;
				}
				else
					currentCustomParameters[i] = 0;
			}
			currentCustomParameters = null;
			return false;
		}
	}

	public void setOutputStream(PrintStream out) {
		this.out = out;
	}

}
